import socket
import ssl
import threading
from functools import partial

from irc.client import IRC, ServerConnection, DCCConnection, NickMask, ip_quad_to_numstr, ip_numstr_to_quad
from irc.buffer import DecodingLineBuffer
from irc.connection import Factory

from kivy.logger import Logger

from amgineirc.model.irc.ircModel import IRCModel

class BetterDecodingLineBuffer(DecodingLineBuffer):
    def __init__(self):
        DecodingLineBuffer.__init__(self)
        self.encoding = 'utf-8'
        self.errors = 'replace'

class IRCClientModel(object):
    """
    the irc client

    needs multiple connections indexed by server
    """
    
    def __init__(self):
        self._dcc_listener = None
        # view models to recieve input from the net
        self._vm = []
        # methods to recieve notification of connect
        self.on_connects = []
        # methods to recieve notification of disconnect
        self.on_disconnects = []
        # Modifications to these shared lists and dict need to be thread-safe
        self.mutex = threading.RLock()
        # setup the irc object and handlers
        self._ircobj = IRCModel(on_connect=self.on_connect,
                                on_disconnect=self.on_disconnect)
        self._ircobj.add_global_handler("dcc_connect", self.on_connect, -1)
        self._ircobj.add_global_handler("disconnect", self.on_disconnect, -1)
        self._ircobj.add_global_handler("dcc_disconnect", self.on_disconnect, -1)
        self._ircobj.add_global_handler("all_events", self.dispatcher, 0)
        self._ircobj.add_global_handler("ping", self.on_ping, -1)
        # handshaking
        self._ctcp_dcc_chat_invite_handler = None
        self._ctcp_dcc_chat_handshake_handler = None
        self._ctcp_dcc_chat_accept_handler = None
        # runner
        self._runner = None
        self._last_disconnect = None
        
    def on_ping(self, connection, event):
        ##('on_pong: %s' % (event.target))
        pass
        
    @property
    def connections(self):
        l = []
        ircobj = self._ircobj
        with ircobj.mutex:
            for i in ircobj.connections:
                if i.connected:
                    at = i.server if isinstance(i, ServerConnection) else i.peeraddress
                    l.append(at)
        return l
        
    @property
    def _connections_count(self):
        return len(self.connections)

    @property
    def is_connected(self):
        ircobj = self._ircobj
        with ircobj.mutex:
            for i in ircobj.connections:
                if i.connected:
                    return True
        return False

    def get_connection(self, domain):
        ircobj = self._ircobj
        with ircobj.mutex:
            for i in ircobj.connections:
                if isinstance(i, ServerConnection):
                    if i.server == domain:
                        return i
        return None

    def get_real_server_name(self, domain):
        conn = self.get_connection(domain)
        if conn is not None:
            return conn.real_server_name
        else:
            return None

    def server_connect(self, domain, port, secure, nickname):
        """Connect using the underlying connection"""
        connection = self._ircobj.server()
        connection.buffer_class = BetterDecodingLineBuffer
        if secure:
            connector = Factory(wrapper=ssl.wrap_socket)
        else:
            connector = Factory()
        connection.connect(domain,
                           port,
                           nickname,
                           connect_factory=connector)
        return connection

    def server_disconnect(self, connection):
        connection.disconnect()
        
    def disconnect_all(self, message=''):
        self._ircobj.disconnect_all(message)

    # handlers
    
    def on_connect(self, socket, event=None):
        starting = self._connections_count == 1
        if starting:
            # do the on connects
            for m in self.on_connects:
                m()
        # find the socket's connection
        ircobj = self._ircobj
        connection = None
        with ircobj.mutex:
            for i in ircobj.connections:
                if i.socket is socket:
                    connection = i
                    break
        # inform relevent vm's of connection
        if connection is not None:
            if isinstance(connection, ServerConnection):
                for vm in self._vm:
                    if vm.server == connection.server:
                        vm.handle_on_connect(connection)
            else: # dcc connection
                for vm in self._vm:
                    vmcon = vm.connection 
                    if isinstance(vmcon, DCCConnection):
                        if vmcon.peeraddress == connection.peeraddress \
                        and vmcon.peerport == connection.peerport:
                            vm.handle_on_connect(connection)
        else:
            Logger.debug('connectin is None')
        # if this is the first connection then start handling input from connections
        if starting:
            self.start()

    def on_disconnect(self, connection, event=None):
        if connection is None:
            # a dcc timeout connection is None
            return
        # do the on disconnects
        for m in self.on_disconnects:
            m()
        # do the view model on disconnect
        if isinstance(connection, ServerConnection):
            at = connection.server
            i = len(self._vm) - 1
            while i >= 0:
                vm = self._vm[i]
                if vm.server == at:
                    vm.handle_on_disconnect()
                i -= 1
        else: # dcc connection
            for vm in self._vm:
                vmcon = vm.connection 
                if isinstance(vmcon, DCCConnection) \
                and vmcon.peeraddress == connection.peeraddress \
                and vmcon.peerport == connection.peerport:
                    vm.handle_on_disconnect()

    def on_ctcp(self, connection, event):
        if len(event.arguments) > 1:
            args = event.arguments[1].split()
            if event.arguments[0] == 'CHAT':
                # invitation to dcc
                address = ip_numstr_to_quad(args[0])
                port = int(args[1])
                # evsource is the privmsg sender
                self._ctcp_dcc_chat_invite_handler(connection,
                                                   event.source,
                                                   address,
                                                   port)

    def on_dccmsg(self, connection, event):
        if connection.dcctype == 'chat':
            if len(event.arguments) > 0:
                args = event.arguments[0].split(None)
                # irc.client creates the dccmsg type for all dcc input.
                # this app only allows for 3 types of dcc chat.
                #   * handshakes
                #   * text messages
                #   * ruleshake request
                #   there fore this can only be a handshake or a text message.
                if len(args) == 4 and args[2] == u'handshake':
                    # handshake
                    # "<source_nick> <source_server> 'handshake' <data>"
                    self._ctcp_dcc_chat_handshake_handler(connection,
                                                          args[0], # nick
                                                          args[1], # server
                                                          args[3]) # handshake
                else:
                    # text message
                    self._dcc_connection_dispatch_vm(connection, event)
        else:
            # ruleshake
            self._dcc_connection_dispatch_vm(connection, event)
        
    # handlers - displatchers

    def dispatcher(self, connection, event):
        """
        Dispatch events to on_<event.type> method, if present.
        """
        if event.type != 'all_raw_messages' \
            and event.type != 'connect' \
            and event.type != 'disconnect' \
            and event.type != 'dcc_connect' \
            and event.type != 'dcc_disconnect' \
            and self._connections_count > 0:
            meth = 'on_' + event.type.lower().strip()
            method = getattr(self, meth, self.dispatcher_vm)
            method(connection, event)

    def dispatcher_vm(self, connection, event):
        Logger.debug('dispatcher_vm: connection=%s, type=%s, target=%s, source=%s' % (str(type(connection)), event.type, event.target, event.source))
        if isinstance(connection, ServerConnection):
            self._server_connection_dispatch_vm(connection, event)
        else:
            self._dcc_connection_dispatch_vm(connection, event)

    def _server_connection_dispatch_vm(self, connection, event):
        # clean the arguments
        args = [i.replace(unichr(65533), '') for i in event.arguments]
        found_target_vm = False
        # target
        if event.target is None:
            target = ''
        else:
            target = event.target
        tlower = target.lower()
        # source
        if '!' in event.source and '@' in event.source:
            nmask = NickMask(event.source)
            source = nmask.nick
        else:
            source = event.source
        slower = source.lower()
        real_target = None
        for i in self._vm:
            if i.server == connection.server:
                itlower = i.target.lower()
                if i.target == target or itlower == tlower \
                   or i.target == source or itlower == slower:
                    real_target = i.target
                    # send the data to the correct vm
                    i.handle_client_event(target,
                                          event.type,
                                          connection.server,
                                          source,
                                          args)
                    break
        if real_target is None:
            # send the data to the server vm
            for i in self._vm:
                if i.target == connection.server:
                    i.handle_client_event(target,
                                          event.type,
                                          connection.server,
                                          source,
                                          args)

    def _dcc_connection_dispatch_vm(self, connection, event):
        if event.type == 'dcc_connect' \
            or event.type == 'dcc_disconnect':
            return
        else:
            for i in self._vm:
                # send the data to the correct vm
                vmcon = i.connection
                if isinstance(vmcon, DCCConnection):
                    if vmcon.peeraddress == connection.peeraddress \
                    and vmcon.peerport == connection.peerport: 
                        i.handle_client_event(event.target,
                                              event.type,
                                              connection.peeraddress,
                                              event.source,
                                              event.arguments)
                        return
            # no view model
            Logger.debug('vm not found')
        
    # dcc

    def dcc_connect(self, address, port, dcctype="chat"):
        """Connect to a DCC peer.

        Arguments:

            address -- IP address of the peer.

            port -- Port to connect to.

        Returns a DCCConnection instance.
        """
        dcc = self._ircobj.dcc(dcctype)
        dcc.connect(address, port)
        return dcc

    def dcc_listen(self, dcctype="chat"):
        """Listen for connections from a DCC peer.

        Returns a DCCConnection instance
        which is in self._ircobj.connections
        """
        if self._dcc_listener is None or \
        self._dcc_listener.peeraddress is not None:
            dcc = self._ircobj.dcc(dcctype)
            self._dcc_listen(dcc)
            self._dcc_listener = dcc
            t = threading.Timer(120, self.unlisten)
            t.start()
        return self._dcc_listener

    def _dcc_listen(self, dcc):
        """Wait for a connection/reconnection from a DCC peer.

        Returns the DCCConnection object.

        The local IP address and port are available as
        self.localaddress and self.localport.  After connection from a
        peer, the peer address and port are available as
        self.peeraddress and self.peerport.
        """
        if dcc.dcctype == 'chat':
            dcc.buffer = BetterDecodingLineBuffer()
        dcc.handlers = {}
        dcc.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        dcc.passive = 1
        try:
            dcc.socket.bind((socket.gethostbyname(socket.gethostname()), 0)) # 59
            dcc.localaddress, dcc.localport = dcc.socket.getsockname()
            dcc.socket.listen(5)
        except socket.error as x:
            raise DCCConnectionError("Couldn't bind socket: %s" % x)

    def send_dcc_chat_request(self, irc_connection, target):
        # start a dcc server
        # send its address and port in the handshake
        dcc = self.dcc_listen()
        ip = ip_quad_to_numstr(dcc.localaddress)
        params = '%s %i' % (ip, dcc.localport)
        irc_connection.ctcp('CHAT', target, params)

    def unlisten(self):
        if self._dcc_listener is not None: 
            ##if not self._dcc_listener.connected:
            ##    self._dcc_listener.connected = 1
            try:
                self._dcc_listener.disconnect()
                self._dcc_listener = None
            except:
                pass

    # model bindings
    
    def register_on_ctcp_dcc_chat_invite(self, method):
        # params to method
        # source, peer_address, peer_port
        self._ctcp_dcc_chat_invite_handler = method

    def register_on_ctcp_dcc_chat_handshake(self, method):
        self._ctcp_dcc_chat_handshake_handler = method
    
    def register_on_ctcp_dcc_chat_accept(self, method):
        # params to method
        # source, dcc_connection
        self._ctcp_dcc_chat_accept_handler = method

    # view model bindings

    def register_on_connect(self, method):
        """
        this is not for connection vm's which 
        registered with register_default_view_model or register_view_model
        since their handle_on_connect and handle_on_disconnect methods will be called
        the vm must have a method which takes 0 args
        this is mainly for the status bar and server settings vm
        """
        self.on_connects.append(method)

    def unregister_on_connect(self, method):
        if method in self.on_connects:
            self.on_connects.remove(method)

    def register_on_disconnect(self, method):
        """
        this is not for connection vm's which 
        registered with register_default_view_model or register_view_model
        since their handle_on_connect and handle_on_disconnect methods will be called
        the vm must have a method which takes 0 args
        this is mainly for the status bar and server settings vm
        """
        self.on_disconnects.append(method)

    def unregister_on_disconnect(self, method):
        if method in self.on_disconnects:
            self.on_disconnects.remove(method)

    def register_view_model(self, vm):
        """
        a vm should register itself here.
        the vm must have these methods:
        handle_client_event(self, evtarget, evtype, evserver, evsource, evargs)
        """
        with self.mutex:
            self._vm.append(vm)

    def unregister_view_model(self, vm):
        """before closing a connection view its vm should call this
        """
        with self.mutex:
            if vm in self._vm:
                i = self._vm.index(vm)
                del(self._vm[i])

    def start(self):
        """
        overridden for threading
        """
        self._runner = threading.Thread(group=None,
                                        target=self.run,
                                        name='IRCRunner')
        self._runner.start()

    def run(self):
        # for threading
        while self._connections_count > 0:
            self._ircobj.process_once(0.5)
